package first.group.ssh;

import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.LinkedList;

public class ConnectionService {

    private static final ConnectionService instance = new ConnectionService();

    private ConnectionService() {
    }

    public static ConnectionService getInstance() {
        return instance;
    }

    private final LinkedList<Connection> connections = new LinkedList<>();
    private Selector selector = null;

    private void ensureSelector() {
        if (selector != null) {
            return;
        }
        try {
            selector = Selector.open();
        } catch (IOException ex) {
            throw new RuntimeException("Exception in opening selector", ex);
        }
    }

    public boolean addConnection(Connection connection) {
        synchronized (this) {
            ensureSelector();
            connections.add(connection);
            var channel = connection.openSocketChannel();
            if (channel == null) {
                return false;
            }
            try {
                var key = channel.register(selector, SelectionKey.OP_CONNECT);
                key.attach(connection);
                wakeup();
            } catch (ClosedChannelException ex) {
                throw new AssertionError();
            }
            start();
        }
        return true;
    }

    public void removeConnection(Connection connection) {
        synchronized (this) {
            connections.remove(connection);
        }
    }

    public void wakeup() {
        if (selector == null) {
            return;
        }
        selector.wakeup();
    }

    private void work() {
        while (selector.isOpen()) {
            for (var key : selector.keys()) {
                Connection connection = (Connection) key.attachment();
                connection.checkState(key);
            }

            synchronized (this) {
                if (connections.isEmpty()) {
                    // As there is no connections, stop the work thread.
                    try {
                        selector.close();
                        selector = null;
                    } catch (IOException ex) {
                        throw new RuntimeException("Exception in closing selector", ex);
                    }
                    break;
                }
            }

            try {
                selector.select();
            } catch (IOException ex) {
                throw new RuntimeException("Exception in selecting", ex);
            }

            var selectedKeys = selector.selectedKeys();
            for (var key : selectedKeys) {
                Connection connection = (Connection) key.attachment();
                connection.processSelection(key);
            }
            selectedKeys.clear();
        }
        thread = null;
    }

    private Thread thread = null;

    public void start() {
        synchronized (this) {
            if (thread != null) {
                return;
            }
            thread = new Thread(() -> {
                work();
            });
            thread.setName("ConnectionServiceThread");
            thread.start();
        }
    }
}
